Run Loops

🐴的,经常看见大神们的文章提到 RunLoop,找了一些网上的文章都看得不明不白。无法可施,还是翻译一下苹果的官方文档吧。


另外推荐看 Ibireme 的这篇文章:深入理解RunLoop,比官方文档易懂很多。

还有 孙源@sunnyxx 的视频:优酷链接


Run loops 是跟线程相关联的底层结构。一个 run loop 是一个事件处理循环(event processing loop),我们可以用它来执行工作,还有处理接收到的事件。Run loop 的目的是为了让你的线程在有任务需要处理时保持 busy,在空闲时进入 sleep 状态以免占用资源。


Run loop 机制并不是完全自动的。你需要设计你线程相关的代码来在合适的时间开始一个 run loop 并且响应到来的事件。Cocoa 跟 Core Foundation 都提供了相应的 run loop 对象来帮助你设置并且管理线程的 run loop。你的程序不需要显式创建他们。每一个线程,包括 app 的主线程,都有一个关联的 run loop 对象。只有二级线程(secondary thread)需要显式运行它们的 run loop。作为 app 启动操作的一部分,app framework 会自动在主线程设置并且运行 run loop。


相关链接:NSRunLoop Class Reference, CFRunLoop Reference

Run loop 剖析

一个 run loop 就像它的名字听起来那样,它是一个循环。而你的线程会进入这个循环并且使用它来处理一些事件。你的代码里面应该提供控制语句来实现 run loop 的循环部分——也就是说,代码中需要使用 while 或者 for 循环来驱动这个 run loop。在这个循环中,你将使用一个 run loop 对象来运行事件处理代码(event-processing)——接受时间并且调用相应的方法。


一个 run loop 接收事件的来源有两种。输入源(input source)会传递异步事件,通常会是来自其他线程或者是 app 的 message。定时器源(timer source)传递同步时间,会在计划时间点或者重复的时间间隔中发出。两种类型的事件源都会在事件到来时,使用一种程序指定的处理规则(application-specific handler routine)来处理事件。


下表展示了一个 run loop 还有两种来源的核心结构。输入源传递异步事件到相应的方法,并且调用 runUntilDate: 方法(在线程关联的 NSRunLoop 对象上调用)来(让 run loop???)退出。定时器源传递事件到相应的 handler 中,但是不会引起 run loop 退出。


image


除了处理输入源,run loops 也会生成一些关于 run loop 的行为的消息。已注册的 run-loop observer 可以接收到这些消息,并且使用它们来在线程中做一些额外的操作。可以使用 Core Foundation 来在你的线程中注册 run-loop observer。

Run loop 的模式

一个 run loop 的模式描述了输入源跟定时器源如何被监听,还有 run loop observer 如何被通知。每一次运行你的 run loop,你都将指定(包括显式或者隐式)一种特定的运行模式。在传递消息的过程中,只有关联该运行模式的 source 才会被监听到,它们才能传递它们的事件。类似地,只有关联了该模式的 observer 接收到 run loop 的通知。关联其他模式的 source 将会 hold on 所有新事件,直到有合适模式的 loop,才会将时间传递过去。


在你的代码中,将通过名称来识别不同的模式。Cocoa 跟 Core Foundatin 定义了默认模式跟及几种常用的模式。你也可以自己命名并且使用自定义的模式。虽然你自定模式时取名可以任意取,但是那些模式的内容可不是任意的。你必须确保添加了一个或者多个 source,timer,或者 run-loop observer 到你创建的模式中。


使用模式来将不需要的 source 的事件过滤掉。大部分情况下,你都会在系统定义的 default mode 下运行你的 run loop。但是一个模型模板(modal panel)可能会在 modal mode 中运行。当在这种模式中,只有与 modal panel 相关的 source 会传递时间到线程中。对于二级线程来说,你可能会使用自定义的 mode 来阻止低优先级的 source 在 time-critical 操作中传递事件。

注意:模式是根据事件的来源(就是上文提到的 source)的类型来区分的,而不是根据事件的类型。例如,你不能使用某种模式来匹配鼠标点击的事件或者键盘事件。你可以使用模式来监听不同的端口集合、暂时挂起定时器,或者改变 source 跟 run loop observer。


下表列出了 Cocoa 跟 Core Foundation 定义的集中标准模式。

Mode Name Description
Default NSDefaultRunLoopModel, kCFRunLoopDefaultModel 大部分情况下都会使用这种模式。大多数情况下,你应该使用这种模式来开始你的 run loop 还有设置你的输入源
Connection NSConnectionReplyMode(Cocoa) Cocoa 使用这种模式与 NSConnection 对象相结合来监听 reply。你应该甚少会使用到这个 mode。
Modal NSModalPanelRunLoopMode(Cocoa) Cocoa 使用这个模式来识别传递给 modal panel 的事件。
Event tracking NSEventTrackingRunLoopMode Cocoa 使用这个模式来限制在鼠标拖拽循环中即将到达的事件,同时还有其他 user interface tracking loops
Common modes NSRunLoopCommonModes(Cocoa), KCFRunLoopCommonModes(Core Foundation) 它代表一个可被设置的组,这个组里面包含一些通常会被使用的模式。关联这个模式的输入源就相当于关联了组里面的其他模式。对于 Cocoa 应用来说,这个组里面默认包含了 default, modal 跟 event tracking 三种模式。而 Core Foundation 中仅仅包含了 default 模式。你可以使用 CFRunLoopAddCommonMode 函数来添加自定义的模式。
输入源

输入源异步传递事件到你的线程中。时间的 source 依赖于输入源的类型,通常是两种类型之一。基于端口(Port-based)的输入源监听 app 中匹配的端口。自定义(Custom)输入源监听自定义源。就你的 run loop 而言,一个输入源是 port-based 或者是 custom 的并不重要。系统会实现两种类型的输入源。两种输入源的唯一不同之处就是:它们是如何被分别的。Port-based 源由内核自动识别,自定义源则需要手动识别。


当你创建一个输入源后,便能将它分配给一个或者多个 run loop 模式。模式会影响在给定的时刻哪些 input source 会被响应。大多数情况下,你会在 default 模式下运行你的 run loop,但是你同时也可以指定自定义的模式。如果一个输入源不处于当前监听的模式,它生成的任何时间都会 hold 住,直到 run loop 在正确的模式下运行。

Port-Based Sources

Cocoa 跟 Core Foundation 通过 port-related 对象跟方法为创建 port-based 的输入源提供内建支持。例如,在 Cocoa 中,你永远不需要直接创建输入源。你只需要简单地创建一个 port 对象并且通过 NSPort 中的方法将 port 添加到 run loop 中。port 对象会创建你需要的输入源,并且配置好给你。


在 Core Foundation 中,你必须要手动创建 port 跟他的 run loop source。在两种情况中,你都需要使用跟 prot opaque type(CFMachPortRef, CFMessagePortRef 或者 CFSocketRef)相关联的函数来创建合适的对象。

自定义输入源

要创建自定义输入源,你需要使用 Core Foundation 中跟 CFRunLoopSourceRef opaque type 相关联的方法。使用几个回调方法来配置输入源。Core Foundation 会在不同的时间按点调用这些方法来配置输入源、处理即将到来的时间,还有当一个 source 从 run loop 中被移除时将它销毁掉。


除了定义当事件来临时自定义源的行为之外,你还需要定义时间的传递机制。事件的传递机制会在几个独立的线程中运行,并且负责为输入源提供它的数据,还有当数据准备好可被处理时通知它。时间传递机制的实现完全取决于你,但是不应该太过复杂。

Cocoa Perform Selector Source

除了 port-based sources 之外,Cocoa 定义了一种自定义输入源,允许你在任意的目标线程中执行方法。跟 port-based source 类似,执行方法的 request 在目标线程中是连续执行的,解决了大多数同步问题。这些问题通常会发生在,多个方法在同个线程中运行的情况。跟 port-based source 不同的是,perform selector source 在它执行完它的方法之后就会将自己从 run loop 中移除。

注意:在 OS X v10.5 之前,perform selector sources 经常被用来向主线程发送消息,但是在 OS X v10.5 之后还有在 iOS 中,你可以使用它们来向任何线程发送消息。


当在其他线程中实行 selector 时,目标线程需要有一个 active run loop。对于你创建的线程,这意味着你需要在代码中显式创建一个 run loop。由于主线程创建并运行了自己的 run loop,当应用在 appdelegate 中调用了 applicationDidFinishLaunching:,你便可以在主线程中开始一个 issuing call。所有 perform selector call 会在一个队列中存放起来,然后 run loop 会在每次循环的迭代中执行这些 call。而不是一次迭代执行一个。


下面列出了在 NSObject 中定义的方法,可以用来在其他线程中执行 selector。由于这些方法是在 NSObject 中声明的,你可以在任何能访问 Objective-C 对象的线程中使用它们,包括 POSIX 线程。这些方法实际上不会创建新的线程来执行 selector。


performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在 app 的主线程的下一次 run loop 周期中执行指定的 selector。这些方法提供了设置,让你可以将当前线程阻塞,直到方法被执行。


performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在你持有的 NSThread 对象的线程中执行指定的 selector。这些方法提供了设置,让你可以将当前线程阻塞,直到 selector 被执行。


performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在当前线程的下一个 run loop 循环中执行指定的 selector,这些方法提供了一个从当前执行代码开始的自动的最小延时(automatic mini delay)。多个在队列中的 selector 按照他们的顺序一个一个执行。


cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
让你可以取消使用上面一组方法发送到当前线程的消息。

定时器源

定时器源同步地传递时间到你的线程中,按照预设的时间。Timer 是一种通知一个线程自己去执行一些任务的方法。例如,一个搜索框可以利用一个定时器来进行自动搜索。这样,用户可以在当想要的搜索结果出现时就停止输入。


虽然它生成基于时间的通知,但定时器并不是实时机制。跟输入源类似,定时器跟你的 run loop 的指定的模式相关联。如果一个定时器并所关联的模式跟当前被 run loop 监听的模式不匹配,它不会被触发,直到你运行一个该定时器支持的模式时,才会被触发。类似地,如果一个定时器在 run loop 正在执行到某个 handler 一半的时候被触发,timer 就会等待到下一次 run loop 来调用他的 handler。如果 run loop 压根就没有运行,那么定时器将永远不会被触发。


你可以设置定时器来生成一些时间,单次或者重复。一个重复的定时器自动根据设定的 firing time 来重新规划下次触发时间点,而不是实际上的触发时间点,就算实际的触发时间有延时。如果 firing time 被延后得太厉害,以至于它错过了一个或者多个被规划的 firing time,定时器对于被错失的时间段只会触发一次。对错过的时间段触发完成之后,timer 会继续按照设定的重复间隔重新设定下个 firing time。

Run Loop Observer

跟 source 相反,source 在适当的同步或者异步时间发生时被触发,而 run loop observer 在 run loop 的执行过程中的特定位置触发。你可以使用 run loop observer 来准备你的线程来处理给定的事件,或者在线程进入休眠之前配置好它。你可以在你的 run loop 中将 run loop observer 跟下面的时间关联起来。

  • 开始进入 run loop
  • 当 run loop 将要处理定时器
  • 当 run loop 将要进入休眠
  • 当 run loop 被唤醒,但是在开始处理唤醒它的时间之前
  • run loop 退出


你可以使用 Core Foundation 来将 run loop observer 添加到 app。要创建一个 run loop observer,你可以创建一个 CFRunLoopObserver 类型的对象。它将 track 你的自定义回调,还有它感兴趣的活动。


跟定时器类似,run-loop observer 可以一次或者重复使用。one-shot observer 将在它被触发之后将自己从 run loop 中移除,而 repeating observer 则仍会跟 run loop 相关联。你可以在创建它的时候指定它是单次还是重复。

事件的 Run Loop 序列

每次运行的时候,你的线程的 run loop 会处理待处理的事件,并且生成通知去通知所有关联的 observer。它进行这些工作的顺序非常确定,如下:

  1. 通知所有 observer,已经进入 run loop 了;
  2. 通知所有 observer,就绪的定时器将要 fire 了(如果有的话) ;
  3. 通知所有 observer,非 port-based input source将要 fire 了(如果有的话);
  4. fire 所有准备好 fire 的 non-port-based input source;
  5. 如果有一个 port-based input source 已经准备好并且在等待 fire,则马上执行它的事件,并且跳到 step 9;
  6. 通知所有 observer 线程准备进入休眠;
  7. 将线程休眠,直到有下面任何一种事件发生:
  • 有一个 prot based input source 的时间到达;
  • 有一个 timer fires;
  • run loop 的过期时间到了;
  • run loop 被显式唤醒;
  1. 通知所有 observer 线程刚被唤醒了;
  2. 执行所有待处理的事件:
  • 如果一个用户定义的定时器 fire,处理那个定时器时间并且重新开始该 loop,跳到 step 2;
  • 如果一个 input source fire,传递该事件;
  • 如果 run loop 是被显式唤醒的,但并没有超时,重新开始该 loop,并跳到 step 2;
  1. 通知所有 observer run loop 已经退出了。


由于发送给 observer 的关于 timer 跟 input source 的通知在那些事件实际发生之前就被传递了,所以通知发生的时间与事件实际上产生的时间之间可能会有间隔。如果事件的时间非常重要,你可以使用 sleep awake-from-sleep 的通知来帮助你将时间与实际事件之间关联起来。


由于定时器跟其他周期性事件是在你运行 run loop 的时候传递的,所以如果错过了 loop 将使那些时间的传递中断。典型的例子就是:你通过进入 loop 然后通过定时器不停地向 application 请求事件,来实现一个 mouse-tracking routine。由于你的代码直接抓取事件,而不是让 application 来正常分发这些事件,所以定时器将直到你的 mouse-tracking routine 退出并将控制权返还给 application 的时候才能触发。(???)


run loop 可以通过 run loop 对象被显式唤醒。其他事件也可能会造成 run loop 被唤醒。例如,添加其他 non-port-based 输入源会唤醒 run loop,好让输入源可以被立即处理,而不是要等到其他事件发生。

什么时候能够使用 Run Loop

仅仅当你为你的 application 创建二级线程的时候才需要显式运行 run loop。app 中主线程的 run loop 是基础架构中最重要的一部分。所以,app framework 提供了运行 main loop 的代码,并且自动开始运行该 loop。iOS 中 UIApplication(OS X 中是 NSApplication) 的 run 方法把“运行 app 的 main loop ”作为正常启动的操作序列的一部分,如果你使用 Xcode 的模板项目来创建 app,你应该永远都不需要显式调用这些操作。


对于二级线程来书,你需要决定是否需要创建一个 run loop,如果有需要的话,自行配置并开始它。在所有情况下,你都不需要创建一个线程的 run loop。例如,如果你使用线程来实行一些长时间运行并且预先决定的任务,那么大概你不应该使用 run loop。而当你希望与线程之间能有更多的交互的时候,可能你就需要一个 run loop。例如,在你打算执行下面这些操作的时候,你需要开始一个 run loop:

  • 使用 port-based 或者是自定义的输入源来跟其他线程之间进行信息传递;
  • 在线程中使用 timer;
  • 在 Cocoa app 中使用任何 performSelector... 方法;
  • 需要将线程留存起来,来执行一些周期性的任务。


如果你选择了要使用 run loop,配置跟设置的过程非常简单。跟所有线程相关的编程一样,你应该规划好在合适的情况下退出你的二级线程。最好能让线程自己退出,来干净地结束掉它,而不是强制它结束。

使用 Run Loop 对象

run loop 对象提供了主要的接口来添加输入源、定时器还有 run-loop observer,然后运行该 loop。每一个线程都有一个 run loop 对象跟它关联。在
Cocoa,这个对象是 NSRunLoop 类的对象。而在低层应用中,它是 CFRunLoopRef 类型的指针。

获取一个 Run Loop 对象

要获取当前线程的一个 run loop,你可以使用以下其一的方法:

  • 在 Cocoa 应用中,使用 NSRunLoop 的类方法 currentRunLoop 来取得一个 NSRunLoop 对象;
  • 使用 CFRunLoopGetCurrent 函数。


虽然他们并不是 toll-free
birdged type,但是你可以在需要的时候从 NSRunLoop 对象中取得 CFRunLoopRef 类型的对象。NSRunLoop 类定义了一个 getCFRunLoop 的方法来返回 CFRunLoopRef 对象,你可以将它传递到 Core Foundation 的代码中。由于两个类型的对象都指向同一个 run loop,你可以在需要的时候混合使用两种类型。

配置 Run Loop

在你为二级线程运行 run loop 之前,你需要至少添加一个输入源或者定时器到 run loop 中。如果一个 run loop 没有任何 source 可以监听,那么它将会在你试图运行它的时候马上退出。


除了添加源之外,你还可以添加 run loop observer,用来检测 run loop 的不同执行状态。添加 run loop observer,可使用 CFRunLoopObserverRef 类型并且使用 CFRunLoopAddObserver 函数。run loop observer 必须要使用 Core Foundation 来创建,即使是在 Cocoa 程序中。


下面的代码,展示了如何将线程的 run loop 跟 run loop observer 之间关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)threadMain
{
//程序使用垃圾回收,所以不需要 autorelease pool
NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];

//创建一个 run loop observer 然后将它与 run loop 之间关联
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
KCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&myRunLoopObserver,
&context
);

if (observer) {
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}

//创建并安排一个定时器
[NSTimer scheduleTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES];

NSInteger loopCount = 10;
do {
//将 run loop 运行10次来让定时器得以触发
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
} while(loopCount);
}

当你为长期存在的线程配置 run loop 的时候,最好至少添加一个输入源来接受消息。虽然你可以在仅仅有一个定时器的情况下进入 run loop,一旦 timer 触发,之后某个时间点 invalidate 之后,会造成 run loop 退出。而添加重复的 timer 则可以让 run loop 留存更长的一段时间,但是会让你的线程周期性地被唤醒,这是另一种有效的轮询形式。相反而言,一个输入源等待事件的发生,在事件发生之前将一直让线程保持休眠。

开始一个 Run Loop

在你的程序中仅仅需要为二级线程开始一个 run loop。一个 run loop 必须最少有一个输入源或者定时器来监听。如果一个都没有关联到 run loop,那么 run loop 将马上退出。


有几种方法来开始一个 run loop,包含下面的方式:

  • 无任何条件;
  • 设定一个时间限制;
  • 以一种特殊的模式;

以无条件的方式进入 run loop 是最简单的选择,但它能实现的功能也最少。以无条件的方式进入 run loop 会将线程放入到一个固定的 loop 中,而该 loop 仅提供给你很少的操控功能。你可以向它添加或者移除输入源,但仅有的一种停止 run loop 的方式是 kill。而且也没有方式可以在自定义模式下运行 run loop。


相比起无条件地运行 run loop,一种更好的运行方式是给 run loop 设置一个过期时间。当使用过期时间,run loop 会一直运行直到事件到达,或者指派的时间过期了。如果有一个事件到来了,那个时间会被分派到 handler 来处理,然后 run loop 就会退出。而你的代码随后可以重新开始该 run loop 来处理下一个事件。相反,如果是过期时间到了,你可以简单地重启 run loop 或者使用那个过期的时间来做一些处理。


除了过期时间之外,你还可以用特殊的模式来运行你的 run loop。模式还有过期时间的设置,两者之间并不是互斥的,他们可以同时使用。模式限制传递事件到 run loop 的 source 的类型。


下面的代码展示了进入线程的代码框架。这个例子的关键部分展示了 run loop 的基本结构。概括来说,代码将添加你的输入源还有定时器到 run loop 中,然后重复的让 run loop 再开始。每次 run loop 返回的时候,都检查一下某些条件是否已经达到了,是否可以退出线程了。样例使用了 Core Foundation run loop,所以可以检查返回的结果,然后知道为什么 run loop 退出。同样也可以使用 NSRunLoop 类以相同的方式来运行 run loop ,如果你正在使用 Cocoa 并且不需要检查返回的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)skeletonThreadMain
{
//如果你没有使用垃圾回收机制的话,在这里设置 autorelease pool
BOOL done = NO;

//将你的 source 或者定时器添加到 run loop 并且做一些其他的设置
do {
//开始一个 run loop,但在 source 处理完成之后退出
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

//如果一个 source 显式停止了 run loop,或者没有
//source 或者定时器,继续还有退出
if ((result == KCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) {
done = YES;
}

//在这里检测其他退出条件,并且如果需要的话设置变量 done 的值

} while(!done);

//在这里清理代码,要确保释放任何分配了的 autorelease pool
}


你也可以递归地运行 run loop。换句话说,你可以调用 CFRunLoopRun, CFRunLoopRunInMode, 或者其他的 NSRunLoop 中的用于从 source 或者定时器的 handler 开始 run loop 的方法。当你这样做的时候,你可以使用任一种你想要的模式来运行 run loop,包括哪些在其他 run loop 中使用的模式。

退出 run loop

有两种方法可以让 run loop 在处理完一个事件之后退出 run loop:

  • 在运行 run loop 的时候设置一个过期时间;
  • 直接让 run loop 退出。


用过期时间来退出 run loop 应该是较常用的。指定一个过期时间,然后让 run loop 在退出之前完成它所有的正常处理操作,包括传递消息到 run loop observer。


显式调用 CFRunLoopStop 函数来产生停止 run loop,效果跟设置过期时间类似。run loop 会将所有剩下的通知发出去,然后退出。不同之处是,你可以对以无条件方式启动的 run loop 使用这个技术。


虽然将 run loop 的 input source 跟 定时器移除也可以引起 run loop 退出,但这并不是一种可靠的退出县城的方式。一些系统程序会添加 input resource 到 run loop 中来处理一些必要的事件。因为你的代码可能不会发现这些 source,所以也不能移除它们,而你的 run loop 也因此而不能退出。

Run Loop 对象跟线程安全

线程安全根据你用来控制你的 loop 的 API 而不同。Core Founcation 的函数通常是线程安全的,并且能被任何线程调用。但是如果你正在执行一些修改 run loop 的配置的操作,最好还是在该 run loop 对应的线程里面进行。


而 Cocoa 中的 NSRunLoop 类并不如它在 Core Foundation 中对应的部分一样线程安全。如果你在使用 NSRunLoop 类来修改你的 run loop,你应该只在该 run loop 对应的同个线程中进行。为属于其他线程的 run loop 添加输入源或者定时器可能会造成你的 app 崩溃或者出现 bug。

配置 Run Loop Source
定义自定义的输入源

创建自定义的输入源需要定义下面这些:

  • 你希望你的输入源处理的信息;
  • scheduler 代码块,能让相关的 client 知道如何访问你的输入源;
  • handler 代码块,可以处理 client 发送的请求;
  • cancellation 代码块,可以取消你的输入源;


由于你创建了一个自定义的输入源来处理自定义信息,所以实际上整个配置的流程被设计得很灵活。scheduler,handler 还有 cancellation 是你在创建自己的自定义输入源时重要的几个环节。然而大部分余下的输入源行为是在 handler 程序之外发生的。例如,如何定义向你的输入源传递数据还有如何让你的输入源与其他线程沟通,完全取决于你。


下表展示了一个样例,设置自定义的输入源。在这里例子中,app 的主线程保留一个指向输入源的的引用、一个自定义的命令缓冲区还有安装该输入源的 run loop。当主线程有一个想要在工作线程处理的任务,它会发送一个命令到命令缓冲区,该命令包含了工作线程开始它的任务所需要的信息。(由于主线程跟工作线程的输入源都拥有该命令缓冲区的引用,所以两者的访问需要被同步。)一旦命令被发送了,主线程会通知输入源,并且唤醒工作线程的 run loop。根据收到的唤醒命令,run loop 会为输入源调用 handler,来处理在 command buffer 中接收到的命令。


image


定义自定义的输入源需要使用 Core Foundation 相关的代码,来配置你的 run loop source,并且将它关联到 run loop。虽然 basic handler 是 C-based 函数,但是这不会妨碍你使用 Objective-C 或者 C++ 来实现你的代码。


上面的示意图中,使用一个 Objective-C 对象来管理命令缓冲区,还有跟 run loop 一起封装起来。再下面的一段代码则展示了如何定义这个对象。RunLoopSource 对象管理一个命令缓冲区,并且使用该命令缓冲区来接受来自其他线程的消息。同时展示了 RunLoopContext 的定义,它是用于传递 RunLoopSource 对象的容器,同时也是 app 的主线程的 run loop 引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray *commands;
}

- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;

//handler method
- (void)sourceFired;

//Client interface, 向 process 注册 command
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;

@end

//CFRunLoopSourceRef 的回调函数
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);

//RunLoopContext 是一个在注册输入源时使用的容器对象
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}

@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;

- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end

虽然上面的 Objective-C 代码管理输入源的自定义数据,但是将输入源关联到 run loop 需要基于 C 的回调函数。这些函数中的第一个在你将一个 run loop source 关联到 run loop 的时候被调用,如下面的代码所示。由于该 input source 只有一个 client(主线程),它使用一个 scheduler 该函数来向注册器自身发送消息。当 delegate 想要跟 input source 沟通,它需要使用 RunLoopContext 对象中的信息。

翻译到这里,我想知道的知道得差不多了,
待续。。。